// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dfa.axis.v1_19.matchtables;

import com.google.api.ads.dfa.axis.factory.DfaServices;
import com.google.api.ads.dfa.axis.v1_19.AdRemote;
import com.google.api.ads.dfa.axis.v1_19.ApiException;
import com.google.api.ads.dfa.axis.v1_19.City;
import com.google.api.ads.dfa.axis.v1_19.CitySearchCriteria;
import com.google.api.ads.dfa.axis.v1_19.Country;
import com.google.api.ads.dfa.axis.v1_19.State;
import com.google.api.ads.dfa.lib.client.DfaSession;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Implementation of LookupObject that will generate match tables for Geographic
 * (Country, City, etc) and Technology (Browser, OS) IDs.
 *
 * @author Moshe Kahan
 */
class GeoTechLookup implements LookupObject {

  // Hard coded values as DFA7 and DFA6 operating systems sets are not equivalent.
  private enum OPERATING_SYSTEM {
    DART_LINUX(8192, "Linux"),
    DART_MAC(128, "Macintosh"),
    DART_MAC_68X(256, "Macintosh"),
    DART_MAC_POWER_PC(512, "Macintosh"),
    DART_WINDOWS_7(4194304, "Microsoft Windows 7"),
    DART_WINDOWS_2000(2, "Microsoft Windows 2000"),
    DART_WINDOWS_95(16, "Microsoft Windows 95"),
    DART_WINDOWS_98(1048576, "Microsoft Windows 98"),
    DART_WINDOWS_NT(32, "Microsoft Windows NT"),
    DART_WINDOWS_VERSION_UNKNOWN(8, "Microsoft Windows Unknown Version"),
    DART_WINDOWS_VISTA(2097152, "Microsoft Windows Vista"),
    DART_WINDOWS_XP(4, "Microsoft Windows XP"),
    DART_UNKNOWN_OS(1, "Other/Unknown"),
    DART_OS2(64, "Other/Unknown"),
    DART_NEXT(262144, "Other/Unknown"),
    DART_UNIX_UNKNOWN(1024, "Unix"),
    DART_OTHER(2048, "Unix"),
    DART_SUN(4096, "Unix"),
    DART_HP(16384, "Unix"),
    DART_AIX(32768, "Unix"),
    DART_OSF1(65536, "Unix"),
    DART_IRIX(131072, "Unix"),
    DART_SGI(524288, "Unix");

    OPERATING_SYSTEM(long id, String name) {
      this.id = id;
      this.name = name;
    }

    long getId() {
      return id;
    }

    String getName() {
      return name;
    }

    private long id;
    private String name;
  }

  // Hard coded values as DFA7 and DFA6 browser sets are not equivalent.
  private enum BROWSER {
    DART_ANDROID(29, "Android"),
    DART_AOL_IWENG(3, "AOL (IWENG)"),
    DART_FIREFOX(26, "Firefox"),
    DART_GOOGLE_CHROME(28, "Google Chrome"),
    DART_MICROSOFT_IE(5, "Microsoft Internet Explorer"),
    DART_MOZILLA(8, "Mozilla"),
    DART_NETSCAPE_NAVIGATOR(1, "Netscape Navigator"),
    DART_OPERA(25, "Opera"),
    DART_UNKNOWN(0, "Other"),
    DART_SAFARI(27, "Safari"),
    DART_IPAD(31, "Safari (iPad)"),
    DART_IPHONE_IPOD_TOUCH(30, "Safari (iPhone/iPod)"),
    DART_WINDOWS_MESSENGER(33, "Windows Live Messenger / MSN Messenger");

    BROWSER(long id, String name) {
      this.id = id;
      this.name = name;
    }

    long getId() {
      return id;
    }

    String getName() {
      return name;
    }

    private long id;
    private String name;
  }

  private int cityColumnIndex;
  private int stateColumnIndex;
  private int countryColumnIndex;
  private int browserColumnIndex;
  private int osColumnIndex;
  private AdRemote adService;
  private Set<String> countrySet;
  private Set<String> citySet;
  private Set<String> stateSet;
  private Set<String> browserSet;
  private Set<String> osSet;
  private TableSettings tableSettings;

  /**
   * Constructor accepts DFA service/session objects needed to create SOAP remotes to the
   * API.
   *
   * @param dfaServices DFA services object needed to create SOAP remotes to the API
   * @param session DFA session object needed to create SOAP remotes to the API
   * @param tableSettings match table application configuration used as a MatchTable object factory
   */
  GeoTechLookup(DfaServices dfaServices, DfaSession session, TableSettings tableSettings) {
    // Request the ad service from the service client factory.
    adService = dfaServices.get(session, AdRemote.class);

    cityColumnIndex = -1;
    stateColumnIndex = -1;
    countryColumnIndex = -1;
    browserColumnIndex = -1;
    osColumnIndex = -1;

    citySet = new HashSet<String>();
    stateSet = new HashSet<String>();
    countrySet = new HashSet<String>();
    browserSet = new HashSet<String>();
    osSet = new HashSet<String>();
    this.tableSettings = tableSettings;
  }

  /**
   * Called when LookupObjectManager is parsing the DT file header. Method stores index
   * of country, state, city, OS and browser columns.
   *
   * @param index in DT file columns
   * @param header name of this DT file field/column
   */
  public void addHeader(int index, String header) {
    if (LookupField.CountryId.getFieldName().equals(header)) {
      countryColumnIndex = index;
    } else if (LookupField.CityId.getFieldName().equals(header)) {
      cityColumnIndex = index;
    } else if (LookupField.StateId.getFieldName().equals(header)) {
      stateColumnIndex = index;
    } else if (LookupField.BrowserId.getFieldName().equals(header)) {
      browserColumnIndex = index;
    } else if (LookupField.OperatingSystemId.getFieldName().equals(header)) {
      osColumnIndex = index;
    }
  }

  /**
   * Called to clear the DT header information' usually because you're processing a new file.
   */
  public void clearHeaders() {
    cityColumnIndex = -1;
    stateColumnIndex = -1;
    countryColumnIndex = -1;
    browserColumnIndex = -1;
    osColumnIndex = -1;
  }

  /**
   * Called after DT file line has been tokenized into array of Strings.  Aggregate over columns
   * of interest.
   *
   * @param values array of tokenized Strings of DT file line
   */
  public void addValues(String[] values) {
    if (countryColumnIndex > -1 && isValid(values[countryColumnIndex])) {
      countrySet.add(values[countryColumnIndex]);
      if (stateColumnIndex > -1 && isValid(values[stateColumnIndex])) {
        stateSet.add(values[stateColumnIndex]);
      }
      if (cityColumnIndex > -1 && isValid(values[cityColumnIndex])) {
        citySet.add(values[cityColumnIndex]);
      }
    }
    if (browserColumnIndex > -1) {
      String browserId = values[browserColumnIndex];
      if (browserId != null && !browserId.isEmpty()) {
        browserSet.add(browserId);
      }
    }
    if (osColumnIndex > -1) {
      String osId = values[osColumnIndex];
      if (osId != null && !osId.isEmpty()) {
        // OS-ID in DT file is a bit mask position.
        Long osLongValue = Long.valueOf(osId);
        String osStringValue = new Long(1 << osLongValue).toString();
        osSet.add(osStringValue);
      }
    }
  }

  private boolean isValid(String value) {
    return (value != null && !value.isEmpty() && !value.equals("0"));
  }

  /**
   * Called when parsing/aggregation is complete. Will lookup via SOAP all unique DFA entries
   * in the DT file and pass data to MatchTable class.
   *
   * @param outputPath location of match table to be generated.
   */
  public void generateMatchTable(String outputPath)
      throws MatchTableFileException, DfaRemoteException {
    MatchTable countryMatchTable = tableSettings.createMatchTable(MatchTable.Type.COUNTRIES);
    MatchTable stateMatchTable = tableSettings.createMatchTable(MatchTable.Type.STATES);
    MatchTable cityMatchTable = tableSettings.createMatchTable(MatchTable.Type.CITIES);
    MatchTable browserMatchTable = tableSettings.createMatchTable(MatchTable.Type.BROWSERS);
    MatchTable osMatchTable = tableSettings.createMatchTable(MatchTable.Type.OPERATING_SYSTEMS);

    Set<Country> countries = new HashSet<Country>();
    // If we're going to generate the Country match table, open it up and write header.
    if (countryMatchTable != null) {
      countryMatchTable.printProgress();
      countryMatchTable.open(outputPath);
      if (!countrySet.isEmpty()) {
        Country[] countryArray = getCountries();
        for (Country country : countryArray) {
          if (countrySet.contains(Long.toString(country.getId()))) {
            countries.add(country);
            countryMatchTable.write(
                Long.toString(country.getId()),
                country.getName());
          }
        }
      }
      countryMatchTable.close();
    }

    // If we're going to generate the City match table, open it up and write header.
    if (cityMatchTable != null) {

      Spinner spinner = new Spinner();
      cityMatchTable.printProgress();
      cityMatchTable.open(outputPath);
      if (!countries.isEmpty() && !citySet.isEmpty()) {
        // Split countries into US versus non-US to not overwhelm SOAP response.
        long[] countryArray = new long[countries.size()];
        int nonUsCountries = 0;
        for (Country country : countries) {
          if (country.getId() == 256) {
            long[] usCountryArray = new long[1];
            usCountryArray[0] = 256;
            spinner.spin();
            City[] usCities = getCities(usCountryArray);
            spinner.stop();
            writeCities(usCities, cityMatchTable);
          } else {
            countryArray[nonUsCountries++] = country.getId();
          }
        }
        if (nonUsCountries > 0) {
          if (nonUsCountries < countryArray.length) {
            countryArray = Arrays.copyOf(countryArray, nonUsCountries);
          }
          spinner.spin();
          City[] cities = getCities(countryArray);
          spinner.stop();
          writeCities(cities, cityMatchTable);
        }
      }
      System.out.println("\r[100.0% of city SOAP lookups completed]");
      cityMatchTable.close();
    }

    // If we're going to generate the State match table, open it up and write header.
    if (stateMatchTable != null) {
      stateMatchTable.printProgress();
      stateMatchTable.open(outputPath);
      if (!countries.isEmpty() && !stateSet.isEmpty()) {
        // Very few countries have state/province info (Canada and U.S. mainly).
        // So we are sending in all countries at once to query.
        long[] countryArray = new long[countries.size()];
        int index = 0;
        for (Country country : countries) {
          countryArray[index++] = country.getId();
        }

        // Write out all states/provinces as only 70 of them or so.
        State[] states = getStates(countryArray);
        for (State state : states) {
          stateMatchTable.write(
              state.getAbbreviation(),
              state.getName());
        }
      }
      stateMatchTable.close();
    }

    // If we're going to generate the Browser match table, open it up and write header.
    if (browserMatchTable != null) {
      browserMatchTable.printProgress();
      browserMatchTable.open(outputPath);
      if (!browserSet.isEmpty()) {
        for (BROWSER browser : BROWSER.values()) {
          if (browserSet.contains(Long.toString(browser.getId()))) {
            browserMatchTable.write(Long.toString(browser.getId()), browser.getName());
          }
        }
      }
      browserMatchTable.close();
    }

    // If we're going to generate the OS match table, open it up and write header.
    if (osMatchTable != null) {
      osMatchTable.printProgress();
      osMatchTable.open(outputPath);
      if (!osSet.isEmpty()) {
        for (OPERATING_SYSTEM os : OPERATING_SYSTEM.values()) {
          // Operating systems stored in DT files are bit positions.
          if (osSet.contains(Long.toString(os.getId()))) {
            osMatchTable.write(Long.toString(os.getId()), os.getName());
          }
        }
      }
      osMatchTable.close();
    }
  }

  private void writeCities(City[] cities, MatchTable cityMatchTable) {
    if (cities != null) {
      for (City city : cities) {
        if (citySet.contains(Long.toString(city.getId()))) {
          cityMatchTable.write(
              Long.toString(city.getId()),
              city.getName());
        }
      }
    }
  }

  private Country[] getCountries() throws DfaRemoteException {
    Country[] countries = new Country[0];
    SoapLookup<Country> countrySoapLookup =
        new SoapLookup<Country>(new CountrySoapLookupCallback());
    List<Country> countryList = new ArrayList<Country>();
    while (countrySoapLookup.hasNext()) {
      Country country = countrySoapLookup.next();
      countryList.add(country);
    }
    return countryList.toArray(countries);
  }

  private City[] getCities(long[] countries) throws DfaRemoteException {
    City[] cities = new City[0];
    SoapLookup<City> citySoapLookup =
        new SoapLookup<City>(new CitySoapLookupCallback(countries));
    List<City> cityList = new ArrayList<City>();
    while (citySoapLookup.hasNext()) {
      City city = citySoapLookup.next();
      cityList.add(city);
    }
    return cityList.toArray(cities);
  }

  private State[] getStates(long[] countries) throws DfaRemoteException {
    State[] states = new State[0];
    SoapLookup<State> stateSoapLookup =
        new SoapLookup<State>(new StateSoapLookupCallback(countries));
    List<State> stateList = new ArrayList<State>();
    while (stateSoapLookup.hasNext()) {
      State state = stateSoapLookup.next();
      stateList.add(state);
    }
    return stateList.toArray(states);
  }

  // SOAPLookupCallback classes
  private class CountrySoapLookupCallback implements SoapLookupCallback<Country> {

    public int getRecords(long[] ids, List<Country> recordList)
        throws ApiException, RemoteException {
      Country[] countries = adService.getCountries();
      for (Country country : countries) {
        recordList.add(country);
      }
      return countries.length;
    }
  }

  private class CitySoapLookupCallback implements SoapLookupCallback<City> {

    CitySoapLookupCallback(long[] countryIds) {
      this.countryIds = countryIds;
    }

    public int getRecords(long[] ids, List<City> recordList)
        throws ApiException, RemoteException {
      CitySearchCriteria citySearchCriteria = new CitySearchCriteria();
      citySearchCriteria.setCountryIds(countryIds);
      City[] cities = adService.getCities(citySearchCriteria);
      if (cities != null) {
        for (City city : cities) {
          recordList.add(city);
        }
        return cities.length;
      }
      return 0;
    }

    private long[] countryIds;
  }

  private class StateSoapLookupCallback implements SoapLookupCallback<State> {

    StateSoapLookupCallback(long[] countryIds) {
      this.countryIds = countryIds;
    }

    public int getRecords(long[] ids, List<State> recordList)
        throws ApiException, RemoteException {
      State[] states = adService.getStates(countryIds);
      if (states != null) {
        for (State state : states) {
          recordList.add(state);
        }
        return states.length;
      }
      return 0;
    }

    private long[] countryIds;
  }
}
